﻿using System;
using System.Linq;
using System.Xml;
using Windows.UI;
using LinkPad.Helpers;
using Windows.Foundation;
using Windows.UI.Xaml.Media;
using Windows.UI.Xaml.Shapes;

namespace LinkPad.Shapes
{
    public sealed class ArrowedLink : BaseManagedLine
    {
        #region Fields

        public readonly static Color DefaultColor = Colors.Red;

        private double _x1;
        private double _y1;
        private double _x2;
        private double _y2;

        private readonly Line _shaft = new Line();
        private readonly Polygon _arrowHead = new Polygon();

        private readonly double _lambda;
        private readonly double _gamma;
        private readonly double _minArrowHead;
        private readonly double _maxArrowHead;

        private bool _boundingBoxInvalid;
        private Rect _boundingBox;

        private bool _suppressUpdateArrow;

        #endregion

        #region Constructors

        public ArrowedLink(ShapeManager shapeManager, double lambda=0.05, double gamma = 0.4, double minArrowHead=3, double maxArrowHead=18)
            : base(shapeManager)
        {
            _arrowHead.Points = new PointCollection {default(Point), default(Point), default(Point)};
            _lambda = lambda;
            _gamma = gamma;
            _minArrowHead = minArrowHead;
            _maxArrowHead = maxArrowHead;

            ShaftStrokeThickness = 4.0;

            Color = DefaultColor;

            UpdateArrow();
        }

        #endregion

        #region Properties

        #region IManagedShape members

        public override Rect BoundingBox
        {
            get
            {
                if (_boundingBoxInvalid)
                {
                    var minX = Math.Min(X1, X2);
                    var maxX = Math.Max(X1, X2);
                    var minY = Math.Min(Y1, Y2);
                    var maxY = Math.Max(Y1, Y2);
                    _boundingBox.X = minX;
                    _boundingBox.Y = minY;
                    _boundingBox.Width = maxX - minX;
                    _boundingBox.Height = maxY - minY;
                    _boundingBoxInvalid = false;
                }
                return _boundingBox;
            }
        }

        #endregion

        public double ShaftStrokeThickness
        {
            get { return _shaft.StrokeThickness; }
            set { _shaft.StrokeThickness = value; }
        }

        public Brush ShaftStroke
        {
            get { return _shaft.Stroke; }
            set { _shaft.Stroke = value; }
        }

        public Brush ArrowHeadFill
        {
            get { return _arrowHead.Fill; }
            set { _arrowHead.Fill = value; }
        }

        public bool HasSingleColor
        {
            get
            {
                var shaftSolidBrush = ShaftStroke as SolidColorBrush;
                return (shaftSolidBrush != null);
            }
        }

        public Color Color
        {
            get
            {
                var shaftSolidBrush = ShaftStroke as SolidColorBrush;
                if (shaftSolidBrush == null)
                {
                    return new Color(); // default colour
                }
                return shaftSolidBrush.Color;
            }
            set
            {
                var shaftSolidBrush = ShaftStroke as SolidColorBrush;
                if (shaftSolidBrush != null)
                {
                    shaftSolidBrush.Color = value;
                }
                var arrowSolidBrush = ArrowHeadFill as SolidColorBrush;
                if (arrowSolidBrush != null)
                {
                    arrowSolidBrush.Color = value;
                }
            }
        }

        public double X1
        {
            get { return _x1; }
            set
            {
                ShapeManager.ShapeChanged(this);
                RealX1 = ShapeManager.GetAbsoluteLocationX(value);
                if (Math.Abs(_x1 - value) < double.Epsilon) return;
                _x1 = value;
                UpdateArrow();
            }
        }

        public double Y1
        {
            get { return _y1; }
            set
            {
                ShapeManager.ShapeChanged(this);
                RealY1 = ShapeManager.GetAbsoluteLocationY(value);
                if (Math.Abs(_y1 - value) < double.Epsilon) return;
                _y1 = value;
                UpdateArrow();
            }
        }

        public double X2
        {
            get { return _x2; }
            set
            {
                ShapeManager.ShapeChanged(this);
                RealX2 = ShapeManager.GetAbsoluteLocationX(value);
                if (Math.Abs(_x2 - value) < double.Epsilon) return;
                _x2 = value;
                UpdateArrow();
            }
        }

        public double Y2
        {
            get { return _y2; }
            set
            {
                ShapeManager.ShapeChanged(this);
                RealY2 = ShapeManager.GetAbsoluteLocationY(value);
                if (Math.Abs(_y2 - value) < double.Epsilon) return;
                _y2 = value;
                UpdateArrow();
            }
        }

        #endregion

        #region Methods

        #region ManagedShape members

        public override void AddToCanvas()
        {
            var canvas = ShapeManager.Canvas;
            canvas.Children.Add(_shaft);
            canvas.Children.Add(_arrowHead);
        }

        public override void RemoveFromCanvas()
        {
            var canvas = ShapeManager.Canvas;
            canvas.Children.Remove(_shaft);
            canvas.Children.Remove(_arrowHead);
        }

        public override int[] IndicesInCanvas()
        {
            var index = ShapeManager.Canvas.Children.IndexOf(_shaft);
            return new[] {index};
        }

        public override void InsertToCanvas(int index)
        {
            ShapeManager.Canvas.Children.Insert(index, _shaft);
            ShapeManager.Canvas.Children.Insert(index, _arrowHead);
        }

        public override double DistanceToPoint(Point point)
        {
            return GeometryHelper.DistanceLineSegmentToPoint(X1, Y1, X2, Y2, point.X, point.Y);
        }

        public override void Transform()
        {
            var scaleX = ShapeManager.ScaleX;
            var scaleY = ShapeManager.ScaleY;
            var offsetX = ShapeManager.OffsetX;
            var offsetY = ShapeManager.OffsetY;

            _suppressUpdateArrow = true;

            X1 = RealX1*scaleX + offsetX;
            Y1 = RealY1*scaleY + offsetY;
            X2 = RealX2*scaleX + offsetX;
            Y2 = RealY2*scaleY + offsetY;

            _suppressUpdateArrow = false;

            UpdateArrow();
        }

        /// <summary>
        ///  Saves the shape to the specified XML writer
        /// </summary>
        /// <param name="xmlWriter">XML writer to write the stream to</param>
        public override void WriteToXml(XmlWriter xmlWriter)
        {
            xmlWriter.WriteStartElement("ArrowedLink");
            xmlWriter.WriteAttributeString("X1", RealX1.ToString());
            xmlWriter.WriteAttributeString("Y1", RealY1.ToString());
            xmlWriter.WriteAttributeString("X2", RealX2.ToString());
            xmlWriter.WriteAttributeString("Y2", RealY2.ToString());
            xmlWriter.WriteAttributeString("Color", Color.ToString());
            xmlWriter.WriteEndElement();
        }

        /// <summary>
        ///  Loads the shape from the specified XML reader
        /// </summary>
        /// <param name="xmlReader">XML reader to read the stream from</param>
        public override bool ReadFromXml(XmlReader xmlReader)
        {
            var paramRead = new[] {false, false, false, false};
            var color = DefaultColor;
            while (xmlReader.MoveToNextAttribute())
            {
                switch (xmlReader.Name)
                {
                    case "X1":
                        RealX1 = Convert.ToDouble(xmlReader.Value);
                        paramRead[0] = true;
                        break;
                    case "Y1":
                        RealY1 = Convert.ToDouble(xmlReader.Value);
                        paramRead[1] = true;
                        break;
                    case "X2":
                        RealX2 = Convert.ToDouble(xmlReader.Value);
                        paramRead[2] = true;
                        break;
                    case "Y2":
                        RealY2 = Convert.ToDouble(xmlReader.Value);
                        paramRead[3] = true;
                        break;
                    case "Color":
                        var sclr = xmlReader.Value;
                        var a = Convert.ToByte(sclr.Substring(1, 2), 16);
                        var r = Convert.ToByte(sclr.Substring(3, 2), 16);
                        var g = Convert.ToByte(sclr.Substring(5, 2), 16);
                        var b = Convert.ToByte(sclr.Substring(7, 2), 16);
                        color = Color.FromArgb(a, r, g, b);
                        break;
                }
            }

            ShaftStroke = new SolidColorBrush(color);
            ArrowHeadFill = new SolidColorBrush(color);

            xmlReader.MoveToElement();

            return paramRead.All(b => b);
        }

        #endregion

        private void UpdateArrow()
        {
            if (_suppressUpdateArrow)
            {
                return;
            }

            var actualLambda = _lambda;
            
            var dxl = Y1 - Y2;
            var dyl = X2 - X1;

            if (_minArrowHead > 0 || _maxArrowHead > 0)
            {
                var dist = Math.Sqrt(dxl*dxl + dyl*dyl);
                var ldist = dist*actualLambda;
                if (_minArrowHead > 0 && ldist < _minArrowHead && dist > _minArrowHead*2)
                {
                    actualLambda = _minArrowHead/dist;
                }
                if (_maxArrowHead > 0 && ldist > _maxArrowHead)
                {
                    actualLambda = _maxArrowHead/dist;
                }
            }

            var xm = (actualLambda * X1 + X2) / (actualLambda + 1);
            var ym = (actualLambda * Y1 + Y2) / (actualLambda + 1);

            _shaft.X1 = X1;
            _shaft.Y1 = Y1;
            _shaft.X2 = xm;
            _shaft.Y2 = ym;

            var dxr = -dxl;
            var dyr = -dyl;
            var r = _gamma*actualLambda;

            var xl = xm + dxl * r;
            var yl = ym + dyl * r;
            var xr = xm + dxr * r;
            var yr = ym + dyr * r;

            System.Diagnostics.Debug.Assert(_arrowHead.Points != null);
            _arrowHead.Points[0] = new Point(X2, Y2);
            _arrowHead.Points[1] = new Point(xl, yl);
            _arrowHead.Points[2] = new Point(xr, yr);

            _boundingBoxInvalid = true;
        }

        #endregion

    }
}

